<?php
/**
 * A class to render Diffs in different formats.
 *
 * This class renders the diff in classic diff format. It is intended that
 * this class be customized via inheritance, to obtain fancier outputs.
 *
 * $Horde: framework/Text_Diff/Diff/Renderer.php,v 1.5.10.10 2008/01/04 10:37:27 jan Exp $
 *
 * Copyright 2004-2008 The Horde Project (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (LGPL). If you did
 * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
 *
 * @package Text_Diff
 */
class Text_Diff_Renderer {

	/**
	 * Number of leading context "lines" to preserve.
	 *
	 * This should be left at zero for this class, but subclasses may want to
	 * set this to other values.
	 */
	var $_leading_context_lines = 0;

	/**
	 * Number of trailing context "lines" to preserve.
	 *
	 * This should be left at zero for this class, but subclasses may want to
	 * set this to other values.
	 */
	var $_trailing_context_lines = 0;

	/**
	 * Constructor.
	 */
	function Text_Diff_Renderer($params = array())
	{
		foreach ($params as $param => $value) {
			$v = '_' . $param;
			if (isset($this->$v)) {
				$this->$v = $value;
			}
		}
	}

	/**
	 * Get any renderer parameters.
	 *
	 * @return array  All parameters of this renderer object.
	 */
	function getParams()
	{
		$params = array();
		foreach (get_object_vars($this) as $k => $v) {
			if ($k[0] == '_') {
				$params[substr($k, 1)] = $v;
			}
		}

		return $params;
	}

	/**
	 * Renders a diff.
	 *
	 * @param Text_Diff $diff  A Text_Diff object.
	 *
	 * @return string  The formatted output.
	 */
	function render($diff)
	{
		$xi = $yi = 1;
		$block = false;
		$context = array();

		$nlead = $this->_leading_context_lines;
		$ntrail = $this->_trailing_context_lines;

		$output = $this->_startDiff();

		$diffs = $diff->getDiff();
		foreach ($diffs as $i => $edit) {
			/* If these are unchanged (copied) lines, and we want to keep
			 * leading or trailing context lines, extract them from the copy
			 * block. */
			if (is_a($edit, 'Text_Diff_Op_copy')) {
				/* Do we have any diff blocks yet? */
				if (is_array($block)) {
					/* How many lines to keep as context from the copy
					 * block. */
					$keep = $i == count($diffs) - 1 ? $ntrail : $nlead + $ntrail;
					if (count($edit->orig) <= $keep) {
						/* We have less lines in the block than we want for
						 * context => keep the whole block. */
						$block[] = $edit;
					} else {
						if ($ntrail) {
							/* Create a new block with as many lines as we need
							 * for the trailing context. */
							$context = array_slice($edit->orig, 0, $ntrail);
							$block[] = new Text_Diff_Op_copy($context);
						}
						/* @todo */
						$output .= $this->_block($x0, $ntrail + $xi - $x0,
						$y0, $ntrail + $yi - $y0,
						$block);
						$block = false;
					}
				}
				/* Keep the copy block as the context for the next block. */
				$context = $edit->orig;
			} else {
				/* Don't we have any diff blocks yet? */
				if (!is_array($block)) {
					/* Extract context lines from the preceding copy block. */
					$context = array_slice($context, count($context) - $nlead);
					$x0 = $xi - count($context);
					$y0 = $yi - count($context);
					$block = array();
					if ($context) {
						$block[] = new Text_Diff_Op_copy($context);
					}
				}
				$block[] = $edit;
			}

			if ($edit->orig) {
				$xi += count($edit->orig);
			}
			if ($edit->final) {
				$yi += count($edit->final);
			}
		}

		if (is_array($block)) {
			$output .= $this->_block($x0, $xi - $x0,
			$y0, $yi - $y0,
			$block);
		}

		return $output . $this->_endDiff();
	}

	function _block($xbeg, $xlen, $ybeg, $ylen, &$edits)
	{
		$output = $this->_startBlock($this->_blockHeader($xbeg, $xlen, $ybeg, $ylen));

		foreach ($edits as $edit) {
			switch (strtolower(get_class($edit))) {
				case 'text_diff_op_copy':
					$output .= $this->_context($edit->orig);
					break;

				case 'text_diff_op_add':
					$output .= $this->_added($edit->final);
					break;

				case 'text_diff_op_delete':
					$output .= $this->_deleted($edit->orig);
					break;

				case 'text_diff_op_change':
					$output .= $this->_changed($edit->orig, $edit->final);
					break;
			}
		}

		return $output . $this->_endBlock();
	}

	function _startDiff()
	{
		return '';
	}

	function _endDiff()
	{
		return '';
	}

	function _blockHeader($xbeg, $xlen, $ybeg, $ylen)
	{
		if ($xlen > 1) {
			$xbeg .= ',' . ($xbeg + $xlen - 1);
		}
		if ($ylen > 1) {
			$ybeg .= ',' . ($ybeg + $ylen - 1);
		}

		// this matches the GNU Diff behaviour
		if ($xlen && !$ylen) {
			$ybeg--;
		} elseif (!$xlen) {
			$xbeg--;
		}

		return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
	}

	function _startBlock($header)
	{
		return $header . "\n";
	}

	function _endBlock()
	{
		return '';
	}

	function _lines($lines, $prefix = ' ')
	{
		return $prefix . implode("\n$prefix", $lines) . "\n";
	}

	function _context($lines)
	{
		return $this->_lines($lines, '  ');
	}

	function _added($lines)
	{
		return $this->_lines($lines, '> ');
	}

	function _deleted($lines)
	{
		return $this->_lines($lines, '< ');
	}

	function _changed($orig, $final)
	{
		return $this->_deleted($orig) . "---\n" . $this->_added($final);
	}

}
